Skip to content

Conversation

@labkey-martyp
Copy link
Contributor

@labkey-martyp labkey-martyp commented Jan 5, 2026

Rationale

Implements the first phase of React conversion of Animal History. This is currently a hidden view until we get to MVP. Link in the ehrAdmin page. Participant history is available if you have the experimental feature on, you'll get a link at the top of the old participant view which comes up when you click on an Id.

Spec is in this PR

Changes

  • Added single/multiple Id search
  • Add All Animal search
  • Add All Alive at Center search
  • Add URL readOnly search for participant history
  • Add URL binding
  • Add new column for reports that are compatible with non-Id filters

@LabKey LabKey deleted a comment from labkey-martyp Jan 6, 2026
Comment on lines +63 to +64
8. Permissions: Folder Read Permission and dataset read permissions
9. Metrics: Report and filter usage.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these requirements or non-requiremnents? I'm unclear on the break between them and the first seven.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requirements

- Shows read-only summary: "Viewing {count} animal(s): {subject1}, {subject2}, ..."
- Shows "Modify Search" button that switches to ID Search mode with current subjects pre-populated
- Reports filter by URL subjects without requiring ID resolution
- No ID limit applies (URL-provided subjects are assumed already validated/resolved)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's worth capping the limit in "normal" usage (for performance, I guess?), it seems like we shouldn't exempt URL-provided values. But I'm also not really clear on why this is a useful mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same UI as participant history which completely derives the filters from the URL. This is pretty much only used when clicking on links generated in the EHR, so we assume we don't need to validate the parameters. In the normal search we're just trying to scope the amount of Ids that can be filtered, but may need to revisit this as we get more testing done.

}, [activeReport]);

const handleFilterChange = useCallback((
newFilterType: 'idSearch' | 'all' | 'aliveAtCenter' | 'urlParams',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a lot of separate hard-codings of these values.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were moved to variables.

2. Likelihood: Medium
3. Mitigation:
1. Id resolution feedback for in UI.
2. Implemented as experimental feature to be able to view old and new animal history results side-by-side.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this feature flag control? Being able to open the new and old versions without toggling an experimental flag seems like it would be more convenient. Or does the flag control which is the target URL by default for existing UI?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the spec. This is going to just be hidden with link in admin page for now.

- Repeat with tab-separated and semicolon-separated lists

4. **Multi-Animal Search (Mixed Separators)**
- Enter IDs using multiple separators in single input: "ID1, ID2\nID3;ID4\tID5"
Copy link
Contributor

@labkey-danield labkey-danield Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because IDs can contain special characters the test should try IDs with separator values (e.g. ID123,A).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

- Verify validation error appears: "Maximum of 100 animal IDs allowed. You entered 101 IDs."
- Verify "Update Report" button is disabled
- Remove one ID to get back to 100
- Verify error clears and "Update Report" button re-enables
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are IDs stored as string values? Would another boundary case be an ID that is a very long string, or MAXINT + 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

4. **Mixed Valid/Invalid IDs:** Resolve valid IDs; show invalid in "Not Found" section
5. **All IDs Not Found:** Display "Not Found" section only; reports panel shows no data message
6. **Alias Resolves to Same ID:** If multiple input values resolve to the same animal ID, show all in "Resolved" section but pass de-duplicated list to reports
7. **Special Characters in IDs:** Support IDs with hyphens, underscores, and other special characters
Copy link
Contributor

@labkey-danield labkey-danield Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be worthwhile to call out some explicit special characters to test. For example, any characters that could be interpreted as a comment(// ' #), string(" ') or something could have special meaning in a SQL query or a url (? @ :).
Also worthwhile to consider where in the ID the special character is. An ID that starts with @123, or '123 or an ID that looks like a link / email address ([email protected])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

- Close browser, reopen bookmark
- Verify exactly 50 animals display correctly
- Verify no ID limit validation (URL Params mode bypasses 100 limit)
- Verify URL hash length doesn't cause browser issues
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another boundary case could be 10 IDs that are 1K characters long (or some other ridiculous size) and bookmark that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

private void clickFilterButton(String buttonText)
{
clickButton(buttonText); // "All Records", "Alive at Center", or "ID Search"
sleep(500); // Allow mode transition
Copy link
Contributor

@labkey-danield labkey-danield Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not opposed to using sleeps, if they work that's great. Sometimes waiting for an element to go stale, the button or some element to show up can make the test a little more reliable and have a quicker run time. Something like:
shortWait().until(ExpectedConditions.stalenessOf(button));

Not sure if that will be applicable, but just a thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to work through a few of these.

@labkey-martyp labkey-martyp marked this pull request as ready for review January 16, 2026 16:33
Copy link
Member

@labkey-tchad labkey-tchad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asserts are out of scope for page (and component) classes. Page classes should provide methods to inspect and control the page but test classes should assert functionality.
If these assertions would be useful elsewhere, you could put them into a separate helper class, maybe.

Comment on lines +173 to +185
waitForElement(Locators.REPORT_TARGET);
// Wait for data region to appear and stabilize
longWait().until(d -> {
try
{
WebElement reportTarget = Locators.REPORT_TARGET.findElement(getDriver());
return DataRegion(getDriver()).timeout(5000).find(reportTarget) != null;
}
catch (Exception e)
{
return false;
}
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A normal wait for this element would give more informative error messages. longWait().until only includes a useful message if you use one of the ExpectedConditions classes.

Suggested change
waitForElement(Locators.REPORT_TARGET);
// Wait for data region to appear and stabilize
longWait().until(d -> {
try
{
WebElement reportTarget = Locators.REPORT_TARGET.findElement(getDriver());
return DataRegion(getDriver()).timeout(5000).find(reportTarget) != null;
}
catch (Exception e)
{
return false;
}
});
WebElement reportTarget = Locators.REPORT_TARGET.refindWhenNeeded(getDriver());
// Wait for data region to appear and stabilize
DataRegion(getDriver()).timeout(5000).waitFor(reportTarget);

Comment on lines +199 to +205
longWait().until(d -> {
boolean buttonReady = !getText(Locators.SEARCH_BY_IDS_BUTTON).contains("Searching");
boolean hasResults = isElementPresent(Locators.REPORT_TARGET) ||
isElementPresent(Locators.ID_RESOLUTION_FEEDBACK) ||
isElementPresent(Locators.VALIDATION_ERROR);
return buttonReady && hasResults;
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a message to the wait to have more informative failures.

Suggested change
longWait().until(d -> {
boolean buttonReady = !getText(Locators.SEARCH_BY_IDS_BUTTON).contains("Searching");
boolean hasResults = isElementPresent(Locators.REPORT_TARGET) ||
isElementPresent(Locators.ID_RESOLUTION_FEEDBACK) ||
isElementPresent(Locators.VALIDATION_ERROR);
return buttonReady && hasResults;
});
longWait().withMessage("Search did not complete").until(d -> {
boolean buttonReady = !getText(Locators.SEARCH_BY_IDS_BUTTON).contains("Searching");
boolean hasResults = isElementPresent(Locators.REPORT_TARGET) ||
isElementPresent(Locators.ID_RESOLUTION_FEEDBACK) ||
isElementPresent(Locators.VALIDATION_ERROR);
return buttonReady && hasResults;
});

public ReactAnimalHistoryPage waitForReportPanelToLoad()
{
// Wait for the report target area to appear
longWait().until(d -> isElementPresent(Locators.REPORT_TARGET));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DRY

Suggested change
longWait().until(d -> isElementPresent(Locators.REPORT_TARGET));
waitForReportToLoad();

// Wait for the report target area to appear
longWait().until(d -> isElementPresent(Locators.REPORT_TARGET));
// Wait for category tabs to be present (indicates panel is fully rendered)
longWait().until(d -> isElementPresent(Locators.CATEGORY_TAB));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use standard element wait for better error message

Suggested change
longWait().until(d -> isElementPresent(Locators.CATEGORY_TAB));
Locators.CATEGORY_TAB.waitForElement(longWait());

public ReactAnimalHistoryPage assertResolvedAliasContains(String inputId, String resolvedId, String aliasType)
{
assertElementPresent(Locators.RESOLVED_SECTION_TITLE);
Locator resolvedItem = Locators.RESOLVED_ITEMS.containing(inputId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partial text match doesn't seem correct here.

Suggested change
Locator resolvedItem = Locators.RESOLVED_ITEMS.containing(inputId);
Locator resolvedItem = Locators.RESOLVED_ITEMS.withText(inputId);

Comment on lines +564 to +572
WebElement searchByIdPanel = Locators.SEARCH_BY_ID_PANEL.findWhenNeeded(this);
WebElement animalIdTextarea = Locators.ANIMAL_ID_TEXTAREA.findWhenNeeded(this);
WebElement searchByIdsButton = Locators.SEARCH_BY_IDS_BUTTON.findWhenNeeded(this);
WebElement allAnimalsButton = Locators.ALL_ANIMALS_BUTTON.findWhenNeeded(this);
WebElement aliveAtCenterButton = Locators.ALIVE_AT_CENTER_BUTTON.findWhenNeeded(this);
WebElement reportTarget = Locators.REPORT_TARGET.findWhenNeeded(this);
WebElement idResolutionFeedback = Locators.ID_RESOLUTION_FEEDBACK.findWhenNeeded(this);
WebElement validationError = Locators.VALIDATION_ERROR.findWhenNeeded(this);
WebElement emptyStatePlaceholder = Locators.EMPTY_STATE_PLACEHOLDER.findWhenNeeded(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ElementCache members should be declared final

Comment on lines +564 to +569
WebElement searchByIdPanel = Locators.SEARCH_BY_ID_PANEL.findWhenNeeded(this);
WebElement animalIdTextarea = Locators.ANIMAL_ID_TEXTAREA.findWhenNeeded(this);
WebElement searchByIdsButton = Locators.SEARCH_BY_IDS_BUTTON.findWhenNeeded(this);
WebElement allAnimalsButton = Locators.ALL_ANIMALS_BUTTON.findWhenNeeded(this);
WebElement aliveAtCenterButton = Locators.ALIVE_AT_CENTER_BUTTON.findWhenNeeded(this);
WebElement reportTarget = Locators.REPORT_TARGET.findWhenNeeded(this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The element cache should only contain elements that won't disappear as long as you remain on the page. Probable just the search panel and its form elements.
Also, the page should actually use the cached elements instead of referencing Locators directly.

Comment on lines +217 to +221
animalHistoryPage
.clearIdInput()
.enterAnimalIds(testAnimalId1, testAnimalId2, testAnimalId3)
.clickSearchByIds()
.waitForReportToLoad()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests shouldn't have to clear inputs and wait for things to load, except in special cases. The page should handle most of that. Searching for some animals should look more like this:

Suggested change
animalHistoryPage
.clearIdInput()
.enterAnimalIds(testAnimalId1, testAnimalId2, testAnimalId3)
.clickSearchByIds()
.waitForReportToLoad()
animalHistoryPage
.searchByIds(testAnimalId1, testAnimalId2, testAnimalId3)

This comment is really for ReactAnimalHistoryPage but, hopefully, it is more clear here

Comment on lines +409 to +411
log("Alive at Center button is disabled - skipping mode activation test");
// Button may be disabled if current report doesn't support non-ID filters
// This is expected behavior based on spec
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this always testing the Demographics report? Why is this check conditionalized?

/**
* Wait for the DataRegion in the report to fully load.
*/
public ReactAnimalHistoryPage waitForDataRegionToLoad()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitForDataRegionToLoad seems redundant. All of the search methods wait for Locators.REPORT_TARGET, getActiveReportDataRegion waits for the data region, and data regions know how to wait for their data to load.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants